"""
    Author:
    Date: 2018-10-25

    微信开发的辅助类.
"""

import hashlib
import json
import random
import requests
import string
import time
from functools import wraps
from urllib import parse

from django.conf import settings
from django.contrib import auth
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.http import HttpResponseServerError
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.views import View

from entities import gm_mgr
from utils import db
from utils import util
from utils import wx_config
from utils import wx_impl
from utils.log import Log

WX_CODE = '_wx_code'
WX_STATE = '_wx_state'


def save_wx_code(request, code = None):
    """ 保存微信的code """
    if code is None:
        code = request.GET.get('code', None)
    if code is not None:
        request.session[WX_CODE] = code
        # Log.info(f'Save request code: {code}')

    state = request.GET.get('state', None)
    if state is not None:
        request.session[WX_STATE] = state
        # Log.info(f'Save request state: {state}')


def get_wx_code(request):
    """ 获取微信的code """
    code = request.GET.get('code', None)
    if code is not None:
        return code

    return request.session.get(WX_CODE, None)


def get_wx_state(request):
    """ 获取微信的state """
    state = request.GET.get('state', None)
    if state is not None:
        return state

    return request.session.get(WX_STATE, None)


def wx_login_required(view_func):
    """ 检测用户是否登录微信账号 """
    @wraps(view_func)
    def new_view_func(request, *args, **kwargs):
        openid = request.session.get('_openid', None)
        if openid is None and not settings.DEBUG:
            openid = wx_get_web_request_openid(request)
            if openid is None:
                raise PermissionDenied
            else:
                request.session['_openid'] = openid

        # Return the HttpResponse object
        response = view_func(request, *args, **kwargs)
        return response

    return new_view_func


def parse_wx_response(rsp):
    """ 解析微信登录返回的json数据，返回相对应的dict, 错误信息 """
    if 200 != rsp.status_code:
        return None, {'code': rsp.status_code, 'msg': 'http error'}

    if isinstance(rsp, HttpResponseServerError):
        Log.error(f'Wx response error: rsp={rsp}')
        return None, {'code': 9998, 'msg': 'HttpResponseServerError'}

    try:
        content = rsp.json()
    except Exception as e:
        Log.error(f'Parse wx response failed: exception={e}, rsp={rsp}')
        return None, {'code': 9999, 'msg': e}

    if 'errcode' in content and content['errcode'] != 0:
        return None, {'code': content['errcode'], 'msg': content['errmsg']}

    return content, None


def wx_get(url, payload):
    """ GET请求微信api """
    rsp = requests.get(url, params=payload)
    return parse_wx_response(rsp)


def wx_post(url, payload):
    """ POST请求微信api """
    payload = json.dumps(payload, ensure_ascii=False).encode('utf-8')
    rsp = requests.post(url, data=payload)
    return parse_wx_response(rsp)


def wx_get_web_request_openid(request):
    """ 获取请求的openid """
    code = get_wx_code(request)
    if code is None:
        Log.error(f'Get request openid failed for code is None!')
        return None

    # 重定向后会带上state参数，开发者可以填写a-zA-Z0-9的参数值，最多128字节
    # state = get_wx_state(request)
    # if state != wx_config.WXWEB_STATE:
    #     Log.error(f'Get request openid failed for invalid state: state={state}')
    #     return None

    api = WechatWebApi()
    token, openid = api.get_access_token(code)
    if token is None or openid is None:
        Log.error(f'Get request openid failed: code={code}!')
        return None
    return openid


def wx_query_red_pack(mch_billno):
    """ 查询发送红包的订单结果 """
    if settings.DEBUG:
        return (True, {
            'return_code': 'SUCCESS',
            'return_msg': 'OK',
            'result_code': 'SUCCESS',
        })

    url = f'{wx_config.DEFAULT_WXAPI_RED_PACK_ORDER_QUERY_URL}'
    nonce_str = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16))

    payload = {
        'nonce_str': nonce_str,  # 随机字符串，不长于32位

        'mch_billno': mch_billno,  # 商户发放红包的商户订单号
        'mch_id': wx_config.MCH_ID,  # 微信支付分配的商户号
        'appid': wx_config.APPID,  # 微信支付分配的公众账号ID
        'bill_type': 'MCHT',  # 订单类型. (MCHT:通过商户订单号获取红包信息。)
    }

    # 计算签名
    temp = '&'.join(['%s=%s' % (key.lower(), payload[key]) for key in sorted(payload)])
    temp += '&key=%s' % wx_config.API_KEY
    payload['sign'] = hashlib.md5(temp.encode('utf-8')).hexdigest()

    payload = util.dict_to_xml(payload)
    rsp = requests.post(url, data=payload, cert=(wx_config.MCH_CERT_PATH, wx_config.MCH_CERT_KEY_PATH))
    wx_ret = util.xml_to_dict(rsp.text)
    if wx_ret['return_code'] != 'SUCCESS':
        return_msg = wx_ret['return_msg']
        Log.error(f'Query order state failed: {return_msg}')
        return False, wx_ret

    if wx_ret['result_code'] != 'SUCCESS':
        err_code = wx_ret['err_code']
        err_code_des = wx_ret['err_code_des']
        Log.error(f'Query order state failed: err_code={err_code}, {err_code_des}')
        return False, wx_ret
    return True, wx_ret


# def wx_query_red_pack(mch_billno):
#     """ 查询发送红包的订单结果 """
#     url = f'{wx_config.DEFAULT_WXAPI_RED_PACK_ORDER_QUERY_URL}'
#     nonce_str = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16))
#
#     payload = {
#         'appid': wx_config.APPID,  # 微信支付分配的公众账号ID
#         'mch_id': wx_config.MCH_ID,  # 微信支付分配的商户号
#         # 'transaction_id': transaction_id,  # 微信的订单号
#         'out_trade_no': mch_billno,  # 商户系统内部订单号
#         'nonce_str': nonce_str,  # 随机字符串，不长于32位
#     }
#
#     # 计算签名
#     temp = '&'.join(['%s=%s' % (key.lower(), payload[key]) for key in sorted(payload)])
#     temp += '&key=%s' % wx_config.WX_API_KEY
#     payload['sign'] = hashlib.md5(temp.encode('utf-8')).hexdigest()
#
#     payload = helper.dict_to_xml(payload, []).encode('utf-8')
#     rsp = requests.post(url, data=payload, cert=(wx_config.MCH_CERT_PATH, wx_config.MCH_CERT_KEY_PATH))
#     wx_ret = helper.xml_to_dict(rsp.text)
#     if wx_ret['return_code'] != 'SUCCESS':
#         return_msg = wx_ret['return_msg']
#         Log.error(f'Query order state failed: {return_msg}')
#         return None
#
#     if wx_ret['result_code'] != 'SUCCESS':
#         err_code = wx_ret['err_code']
#         err_code_des = wx_ret['err_code_des']
#         Log.error(f'Query order state failed: err_code={err_code}, {err_code_des}')
#         return None
#     return wx_ret


def wx_send_red_pack(openid, amount, mch_billno, wishing, act_name, remark):
    """ 给指定用户发送普通红包

        参见文档: https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_4&index=3
    """
    if settings.DEBUG:
        return (True, {
            'return_code': 'SUCCESS',
            'return_msg': 'OK',
            'result_code': 'SUCCESS',
        })

    url = f'{wx_config.DEFAULT_WXAPI_SEND_RED_PACK_URL}'
    nonce_str = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16))

    payload = {
        'nonce_str': nonce_str,  # 随机字符串，不长于32位
        'mch_billno': mch_billno,  # 商户订单号（每个订单号必须唯一。取值范围：0~9，a~z，A~Z）
        'mch_id': wx_config.MCH_ID,  # 微信支付分配的商户号
        'wxappid': wx_config.APPID,  # 微信分配的公众账号ID
        'send_name': wx_config.MCH_NAME,  # 红包发送者名称

        're_openid': openid,  # 接受红包的用户openid
        'total_amount': amount,  # 付款金额，单位分
        'total_num': 1,  # 红包发放总人数
        'wishing': wishing,  # 红包祝福语
        'client_ip': wx_config.WX_AGENT_RED_PACK_CLIENT_IP,  # 调用接口的机器Ip地址
        'act_name': act_name,  # 活动名称
        'remark': remark,  # 备注信息
        'scene_id': 'PRODUCT_5',  # PRODUCT_5:渠道分润
    }

    temp = '&'.join(['%s=%s' % (key.lower(), payload[key]) for key in sorted(payload)])
    temp += '&key=%s' % wx_config.API_KEY
    payload['sign'] = hashlib.md5(temp.encode('utf-8')).hexdigest()

    payload = util.dict_to_xml(payload)
    rsp = requests.post(url, data=payload, cert=(wx_config.MCH_CERT_PATH, wx_config.MCH_CERT_KEY_PATH))
    wx_ret = util.xml_to_dict(rsp.text)
    if wx_ret['return_code'] != 'SUCCESS':
        return_msg = wx_ret['return_msg']
        Log.error(f'Send red pack failed: {return_msg}')
        return False, wx_ret

    if wx_ret['result_code'] != 'SUCCESS':
        err_code = wx_ret['err_code']
        err_code_des = wx_ret['err_code_des']
        Log.error(f'Send red pack failed: err_code={err_code}, {err_code_des}')
        return False, wx_ret
    return True, wx_ret


def get_wx_sign(data_dict, key):
    """ 签名函数，参数为签名的数据和密钥 """
    params_list = sorted(data_dict.items(), key=lambda e: e[0], reverse=False)  # 参数名ASCII码从小到大排序（字典序）
    params_str = "&".join(u"{}={}".format(k, v) for k, v in params_list) + '&key=' + key
    # 组织参数字符串并在末尾添加商户交易密钥
    md5 = hashlib.md5()  # 使用MD5加密模式
    md5.update(params_str.encode('utf-8'))  # 将参数字符串传入
    sign = md5.hexdigest().upper()  # 完成加密并转为大写
    return sign


class JsapiUtil:
    def __init__(self, jsapi_ticket, url):
        self.ret = {
            'nonceStr': util.random_str(15),
            'jsapi_ticket': jsapi_ticket,
            'timestamp': int(time.time()),
            'url': url
        }

    def sign(self):
        temp = '&'.join(['%s=%s' % (key.lower(), self.ret[key]) for key in sorted(self.ret)])
        self.ret['signature'] = hashlib.sha1(temp.encode('utf-8')).hexdigest()
        return self.ret


class WechatApi(object):
    """ 微信公众平台API调用封装类 """
    @staticmethod
    def fetch_access_token():
        """ 向微信后台请求access token
            请求失败会返回None
        """
        if settings.DEBUG:
            Log.error(f'Debug mode cannot fetch access token.')
            return None

        url = wx_config.DEFAULT_WXAPI_GET_ACCESS_TOKEN_URL
        params = {
            'grant_type': 'client_credential',
            'appid': wx_config.APPID,
            'secret': wx_config.APPSECRET,
        }
        data, err = wx_get(url, params)
        if err:
            Log.error(f'Fetch wechat api access token failed: {err}')
            return None

        # 缓存token
        access_token = data['access_token']
        expires_in = data['expires_in']
        expires = int(time.time()) + expires_in
        db.redis_set(wx_config.WXAPI_ACCESS_TOKEN_KEY, access_token)
        db.redis_set(wx_config.WXAPI_ACCESS_TOKEN_EXPIRES_KEY, expires)

        Log.info(f'Cache wechat API access token: {access_token}')
        return access_token

    @staticmethod
    def get_access_token():
        """ 获取access token, 首先尝试从缓存中提取，如果没有缓存或者过期，则重新申请 """
        access_token = db.redis_get(wx_config.WXAPI_ACCESS_TOKEN_KEY)
        access_token_expires = db.redis_get(wx_config.WXAPI_ACCESS_TOKEN_EXPIRES_KEY)
        if access_token and access_token_expires and int(time.time()) < int(access_token_expires):
            return access_token

        new_access_token = WechatApi.fetch_access_token()
        return new_access_token

    @staticmethod
    def fetch_jsapi_ticket(access_token):
        """ 向微信后台请求JSAPI的票据
            请求失败会返回None
        """
        url = wx_config.DEFAULT_WXAPI_GET_JSAPI_TICKET_URL
        params = {
            'access_token': access_token,
            'type': 'jsapi',
        }
        data, err = wx_get(url, params)
        if err:
            Log.error(f'Fetch wechat jsapi ticket failed: {err}')
            return None

        # 缓存token
        jsapi_ticket = data['ticket']
        expires_in = data['expires_in']
        expires = int(time.time()) + expires_in
        db.redis_set(wx_config.WXAPI_JSAPI_TICKET_KEY, jsapi_ticket)
        db.redis_set(wx_config.WXAPI_JSAPI_TICKET_EXPIRES_KEY, expires)

        Log.info(f'Cache wechat API jsapi ticket: {jsapi_ticket}')
        return jsapi_ticket

    @staticmethod
    def get_jsapi_ticket():
        """ 获取JSAPI的票据, 首先尝试从缓存中提取，如果没有缓存或者过期，则重新申请 """
        access_token = WechatApi.get_access_token()
        if access_token is None:
            return None

        jsapi_ticket = db.redis_get(wx_config.WXAPI_JSAPI_TICKET_KEY)
        jsapi_ticket_expires = db.redis_get(wx_config.WXAPI_JSAPI_TICKET_EXPIRES_KEY)
        if jsapi_ticket and jsapi_ticket_expires and int(time.time()) < int(jsapi_ticket_expires):
            return jsapi_ticket

        new_jsapi_ticket = WechatApi.fetch_jsapi_ticket(access_token)
        return new_jsapi_ticket

    @staticmethod
    def get_user_tags():
        """ 获取用户标签列表
            获取失败返回空数组
        """
        access_token = WechatApi.get_access_token()
        url = wx_config.DEFAULT_WXAPI_GET_TAGS_URL
        params = {
            'access_token': access_token,
        }
        data, err = wx_get(url, params)
        if err:
            Log.error(f'Get user tags failed: {err}')
            return []
        return data['tags']

    @staticmethod
    def set_menu(payload):
        """ 设置自定义菜单
            设置成功会返回True, 失败返回False
        """
        access_token = WechatApi.fetch_access_token()
        url = f'{wx_config.DEFAULT_WXAPI_SET_MENU}?access_token={access_token}'
        data, err = wx_post(url, payload)
        if err:
            Log.error(f'Set wx menu failed: {err}')
            return False
        return True

    @staticmethod
    def set_custom_menu(payload):
        """ 设置个性化菜单
            设置成功会返回menu id，失败则返回None
        """
        access_token = WechatApi.fetch_access_token()
        url = f'{wx_config.DEFAULT_WXAPI_SET_CUSTOM_MENU}?access_token={access_token}'
        data, err = wx_post(url, payload)
        if err:
            Log.error(f'Set wx custom menu failed: {err}')
            return None
        return data['menuid']

    # @staticmethod
    # def delete_custom_menu(menuid):
    #     """ 删除个性化菜单
    #         设置成功会返回menu id，失败则返回None
    #     """
    #     access_token = WechatApi.fetch_access_token()
    #     url = f'{wx_config.DEFAULT_WXAPI_SET_CUSTOM_MENU}?access_token={access_token}'
    #     data, err = wx_post(url, payload)
    #     if err:
    #         Log.error(f'Set wx custom menu failed: {err}')
    #         return None
    #     return data['menuid']

    @staticmethod
    def create_qr_ticket(scene_str):
        """ 创建临时二维码ticket
            scene_str为场景字符串形式的场景ID， 长度限制为[1, 64]

            创建成功后返回的数据为{ticket, url, expire_seconds}, 创建失败返回None
        """
        access_token = WechatApi.fetch_access_token()
        if access_token is None:
            return None

        url = f'{wx_config.DEFAULT_WXAPI_CREATE_QR_TICKET}?access_token={access_token}'
        payload = {
            "expire_seconds": 604800,
            "action_name": "QR_STR_SCENE",
            "action_info": {"scene": {"scene_str": scene_str}}
        }
        data, err = wx_post(url, payload)
        if err:
            Log.error(f'Create QR ticket failed: {err}')
            return None
        return data

    @staticmethod
    def get_qr_code_url(scene_str):
        """ 根据二维码ticket换取二维码图片的URL """
        data = WechatApi.create_qr_ticket(scene_str)
        if data is None:
            return None

        url = parse.quote(data['url'])
        return url

    @staticmethod
    def get_qr_code_show_url(scene_str):
        """ 根据二维码ticket换取二维码图片的下载地址 """
        data = WechatApi.create_qr_ticket(scene_str)
        if data is None:
            return None

        ticket = parse.quote(data['ticket'])
        return f'{wx_config.DEFAULT_WXAPI_SHOW_QR_CODE}?ticket={ticket}'
        # url = wx_config.DEFAULT_WXAPI_SHOW_QR_CODE
        # params = {
        #     'ticket': ticket,
        # }
        # data, err = wx_get(url, parse.urlencode(params))
        # if err:
        #     Log.error(f'Show QR code failed: {err}')
        #     return False
        # return True

    @staticmethod
    def get_user_info(openid):
        """ 获取用户基本信息

            正常情况下，微信会返回下述JSON数据包给公众号：
            {
                "subscribe": 1,
                "openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M",
                "nickname": "Band",
                "sex": 1,
                "language": "zh_CN",
                "city": "广州",
                "province": "广东",
                "country": "中国",
                "headimgurl":"http://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
                "subscribe_time": 1382694957,
                "unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"
                "remark": "",
                "groupid": 0,
                "tagid_list":[128,2],
                "subscribe_scene": "ADD_SCENE_QR_CODE",
                "qr_scene": 98765,
                "qr_scene_str": ""
            }
        """
        if settings.DEBUG:
            return {
                'openid': openid,
                'unionid': f'u-{openid}',
                'nickname': openid,
                'headimgurl': 'http://misc.app.res.mqnetswork.com/test_robot_avatar.png',
                'sex': 1,
                'country': '',
                'province': '',
                'city': '',
            }

        access_token = WechatApi.get_access_token()
        if access_token is None:
            return None

        url = wx_config.DEFAULT_WXAPI_GET_USER_INFO
        params = {
            'access_token': access_token,
            'openid': openid,
        }
        data, err = wx_get(url, parse.urlencode(params))
        if err:
            Log.error(f'Get user info failed: {err}')
            return None

        if 'unionid' not in data:
            Log.error(f'Not find unionid in user profile: {data}')
        return data

    @staticmethod
    def get_user_unionid_from_openid(openid):
        """ 根据用户的openid获取用户的unionid """
        user_info = WechatApi.get_user_info(openid)
        if user_info is None:
            return None
        return user_info['unionid']

    @staticmethod
    def tag_user(tagid, openid):
        """ 给用户打标签 """
        access_token = WechatApi.get_access_token()
        if access_token is None:
            return False

        url = f'{wx_config.DEFAULT_WXAPI_TAG_USER_URL}?access_token={access_token}'
        payload = {
            "openid_list": [openid],
            "tagid": tagid,
        }
        data, err = wx_post(url, payload)
        if err:
            Log.error(f'Tag {tagid} to user({openid}) failed: {err}')
            return False
        return True

    @staticmethod
    def untag_user(tagid, openid):
        """ 取消用户标签 """
        access_token = WechatApi.get_access_token()
        if access_token is None:
            return False

        url = f'{wx_config.DEFAULT_WXAPI_UNTAG_USER_URL}?access_token={access_token}'
        payload = {
            "openid_list": [openid],
            "tagid": tagid,
        }
        data, err = wx_post(url, payload)
        if err:
            Log.error(f'Tag {tagid} to user({openid}) failed: {err}')
            return False
        return True

    @staticmethod
    def wx_pay_unified_order(openid, total_fee, out_trade_no, mch_id):
        """ 访问微信支付统一下单接口
            官方文档: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
         """
        params = {
            'appid': wx_config.APPID,  # 公众账号ID
            # 'mch_id': wx_config.MCH_ID,  # 商户号
            'mch_id': mch_id,  # 商户号
            'nonce_str': util.random_str(16),  # 随机字符串
            'body': '钻石',  # 商品描述
            'out_trade_no': out_trade_no,  # 商户系统内部订单号，要求32个字符内，只能是数字、大小写字母_-|* 且在同一个商户号下唯一。
            'total_fee': int(total_fee),  # 订单总金额，单位为分
            'spbill_create_ip': wx_config.SERVER_IP,  # 发送请求服务器的IP地址
            'notify_url': wx_config.WXWEB_PAY_NOTIFY_URL,  # 异步接收微信支付结果通知的回调地址，通知url必须为外网可访问的url，不能携带参数。
            'trade_type': 'JSAPI',  # 交易类型
            'openid': openid,  # trade_type=JSAPI时（即JSAPI支付），此参数必传，此参数为微信用户在商户对应appid下的唯一标识。
        }
        params['sign'] = get_wx_sign(params, wx_config.API_KEY)
        xml = util.dict_to_xml(params)
        response = requests.request('post', wx_config.DEFAULT_UNIFIED_ORDER_URL, data=xml)
        content = response.content
        return content

    @staticmethod
    def get_choose_wx_pay_params(uid, openid, rmb, out_trade_no, mch_id):
        """ 获取微信的Jsapi支付需要的参数 """
        # 获取预支付id
        total_fee = rmb * 100

        # 测试账号
        if uid in settings.TEST_UIDS_FOR_PAY:
            total_fee = 1

        notify_result = WechatApi.wx_pay_unified_order(openid, total_fee, out_trade_no, mch_id)
        prepay_id = util.xml_to_dict(notify_result).get('prepay_id', None)
        if prepay_id is None:
            Log.error(f'Get prepay id for user-{uid} failed: openid={openid}, fee={total_fee}, out_trade_no={out_trade_no}, ret={notify_result}')
            return None
        Log.info(f'Get prepay id for user-{uid} OK: openid={openid}, fee={total_fee}, out_trade_no={out_trade_no}, prepay_id={prepay_id}')

        # 计算paySign
        # 官方文档: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
        # paySign 采用统一的微信支付 Sign 签名生成方法，注意这里 appId 也要参与签名。
        # appId 与 config 中传入的 appId 一致，即最后参与签名的参数有appId, timeStamp, nonceStr, package, signType。
        timestamp = int(time.time())
        noncestr = util.random_str(16)
        package = f'prepay_id={prepay_id}'
        sign_type = 'MD5'
        pay_sign = get_wx_sign({
            'appId': wx_config.APPID,
            "timeStamp": timestamp,
            'nonceStr': noncestr,
            'package': package,
            'signType': sign_type
            }, wx_config.API_KEY)

        choose_wx_pay_params = {
            'appId': wx_config.APPID,

            'timestamp': f'{timestamp}',
            'nonceStr': noncestr,
            'package': package,
            'signType': sign_type,
            'paySign': pay_sign,
        }
        return choose_wx_pay_params

    @staticmethod
    def wx_pay_unified_order_for_h5(total_fee, diamond, out_trade_no, client_ip, wap_name, mch_id):
        """ 访问微信支付统一下单接口
            官方文档: https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=9_20&index=1
         """
        wap_url = 'https://m.yyzb.com/mweb/pay'
        scene_info = '{"h5_info": {"type":"Wap","wap_url": "%s","wap_name": "%s"}}' % (wap_url, wap_name)
        params = {
            'appid': wx_config.APPID,  # 公众账号ID
            # 'mch_id': wx_config.MCH_ID,  # 商户号
            'mch_id': mch_id,  # 商户号
            'nonce_str': util.random_str(16),  # 随机字符串
            'body': '钻石',  # 商品描述
            'out_trade_no': out_trade_no,  # 商户系统内部订单号，要求32个字符内，只能是数字、大小写字母_-|* 且在同一个商户号下唯一。
            'total_fee': int(total_fee),  # 订单总金额，单位为分
            'spbill_create_ip': client_ip,  # 必须传正确的用户端IP,支持ipv4、ipv6格式
            'notify_url': wx_config.WXWEB_PAY_NOTIFY_URL,  # 异步接收微信支付结果通知的回调地址，通知url必须为外网可访问的url，不能携带参数。
            'trade_type': 'MWEB',  # 交易类型
            'scene_info': scene_info,  # 场景信息
        }
        params['sign'] = get_wx_sign(params, wx_config.API_KEY)
        xml = util.dict_to_raw_xml(params)
        response = requests.request('post', wx_config.DEFAULT_UNIFIED_ORDER_URL, data=xml)
        content = response.content
        return content

    @staticmethod
    def get_wx_pay_for_h5_params(uid, rmb, out_trade_no, client_ip, wap_name, mch_id):
        """ 微信h5页面支付的参数 """
        # 获取预支付id
        total_fee = rmb * 100

        # 测试账号
        if uid in settings.TEST_UIDS_FOR_PAY:
            total_fee = 1

        # 调用微信接口
        xml_result = WechatApi.wx_pay_unified_order_for_h5(total_fee, rmb, out_trade_no, client_ip, wap_name, mch_id)
        dict_result = util.xml_to_dict(xml_result)

        # 检测调用结果
        return_code = dict_result.get('return_code', None)
        if return_code != 'SUCCESS':
            return_msg = dict_result.get('return_msg', None)
            Log.error(f'Weixin pay for h5 failed: user-{uid}, fee={total_fee}, out_trade_no={out_trade_no}, return_code={return_code}, msg={return_msg}, ret={xml_result}')
            return None

        result_code = dict_result.get('result_code', None)
        if result_code != 'SUCCESS':
            Log.error(f'Weixin pay for h5 failed: user-{uid}, fee={total_fee}, out_trade_no={out_trade_no}, result_code={result_code}, ret={xml_result}')
            return None

        # 验签
        sign = dict_result.get('sign', None)
        del dict_result['sign']
        checked_sign = get_wx_sign(dict_result, wx_config.API_KEY)
        if sign != checked_sign:
            Log.error(f'Weixin pay for h5 failed for invalid sign: user-{uid}, fee={total_fee}, out_trade_no={out_trade_no}, checked_sign={checked_sign}, ret={xml_result}')
            return None

        prepay_id = dict_result.get('prepay_id', None)
        mweb_url = dict_result.get('mweb_url', None)
        params = {
            'prepay_id': prepay_id,
            'mweb_url': mweb_url,
        }
        return params


class WechatWebApi(object):
    def __init__(self):
        self.config = wx_config
        self._access_token = None
        self._openid = None

    def get_code_url(self):
        """ 微信内置浏览器获取网页授权code的url """
        params = {
            'appid': self.config.APPID,
            'redirect_uri': self.config.WXWEB_REDIRECT_URI,
            'response_type': 'code',
            'scope': self.config.WXWEB_SCOPE,
            'state': self.config.WXWEB_STATE
        }
        encoded_params = parse.urlencode(params)

        url = self.config.DEFAULT_WXWEB_GET_CODE_URL
        url = '%s?%s#wechat_redirect' % (url, encoded_params)
        return url

    def get_access_token_from_cache(self, code):
        """ 从缓存中获取access token """
        key = f'wx_code:{code}'
        Log.info(f'Get cached access token: {key}')
        data = db.redis_get(key)
        if not data:
            return None

        m = json.loads(data)
        access_token = m.get('access_token', '')
        openid = m.get('openid', '')
        return access_token, openid

    def save_access_token_to_cache(self, code, access_token, openid):
        """ 缓存 access token """
        data = {
            'access_token': access_token,
            'openid': openid,
        }
        key = f'wx_code:{code}'
        db.redis_set(key, json.dumps(data))
        db.redis_expire(key, 2*3600)

    def get_access_token(self, code):
        """获取access_token"""
        # 先充缓存中提取 access token 和 openid
        cached_data = self.get_access_token_from_cache(code)
        if cached_data is not None:
            return cached_data

        # 没有缓存的时候继续请求
        url = self.config.DEFAULT_WXWEB_GET_ACCESS_TOKEN_URL
        params = {
            'appid': self.config.APPID,
            'secret': self.config.APPSECRET,
            'code': code,
            'grant_type': 'authorization_code'
        }
        data, err = wx_get(url, params)
        if err:
            Log.error(f'Get access token failed: err={err}')
            # ret = json.loads(err)
            error_code = err.get('code', 0)
            if error_code != 40029:
                # invalid code
                Log.error(f'Get access token failed for code {code}: {err}')
            return None, None

        # 缓存access token
        self._access_token = data['access_token']
        self._openid = data['openid']
        self.save_access_token_to_cache(code, self._access_token, self._openid)

        return self._access_token, self._openid

    def get_user_info(self, access_token, openid):
        """获取用户信息"""
        url = self.config.DEFAULT_WXWEB_GET_USER_INFO_URL
        params = {
            'access_token': access_token,
            'openid': openid,
            'lang': 'zh_CN',
        }
        return wx_get(url, params)


# ------------------------------------------------------------------------------
# VIEW
# ------------------------------------------------------------------------------

def wx_web_auth_view(request):
    """ 请求微信验证授权页面
        重定向请求微信接口，可获得点击用户的code值
    """
    request_ip = util.get_request_real_ip(request)
    Log.info(f'wx web auth from {request_ip}')
    api = WechatWebApi()
    url = api.get_code_url()
    return redirect(url)


def wx_web_auth_callback_first_view(request):
    """ 微信网页授权的首次回调页面
        根据微信发送来的参数验证signature，并返回对应的echostr

        !!该函数仅在首次验证token的时候使用
    """
    Log.info(f'Receive wx web auth callback: {request.GET}')

    try:
        signature = request.GET.get('signature')
        timestamp = request.GET.get('timestamp')
        nonce = request.GET.get('nonce')
        echostr = request.GET.get('echostr')
        token = "orit2019"  # 请按照公众平台官网->基本配置中信息填写

        # 计算hashcode
        temp_list = [token, timestamp, nonce]
        temp_list.sort()
        sha1 = hashlib.sha1()
        x = ''.join(temp_list)
        sha1.update(x.encode('utf-8'))
        hashcode = sha1.hexdigest()

        # 验证签名
        if hashcode != signature:
            Log.error(f'Wx web auth failed for invalid signature: signature={signature}, calcd={hashcode}')
            return HttpResponse("")

        return HttpResponse(echostr)
    except Exception as Argument:
        Log.error(f"wechat web auth callback failed: {Argument}")
        return HttpResponse("")


class WxWebAuthCallbackView(View):
    """ 微信网页授权的回调页面
        根据返回的code获取用户的access token
    """

    def get(self, request):
        Log.info(f'WX GET: {request.body}')
        code = request.GET.get('code', None)
        if code is None:
            return

        api = WechatWebApi()

        # 重定向后会带上state参数，开发者可以填写a-zA-Z0-9的参数值，最多128字节
        state = request.GET.get('state', None)
        if state != api.config.WXWEB_STATE:
            return HttpResponseServerError('Invalid STATE')

        token, openid = api.get_access_token(code)
        if token is None or openid is None:
            return HttpResponseServerError('Get access token failed!')

        user_info, error = api.get_user_info(token, openid)
        if error:
            return HttpResponseServerError(f'Get user info failed: {error}')

        request.session['_openid'] = openid
        user = gm_mgr.get_gm(openid)
        if user is None:
            # user_profile = {
            #     'nickname': user_info['nickname'],
            #     'headimgurl': user_info['headimgurl'],
            #     'openid': user_info['openid'],
            #     'sex': user_info['sex'],
            #     'country': user_info['country'].encode('iso8859-1').decode('utf-8'),
            #     'province': user_info['province'].encode('iso8859-1').decode('utf-8'),
            #     'city': user_info['city'].encode('iso8859-1').decode('utf-8'),
            # }
            # user = gm_mgr.create_gm(openid, user_info)
            return HttpResponseRedirect('/agent/')

        # if user is None:
        #     return HttpResponseServerError('Create user info failed!')

        # 授权登录成功，进入主页
        if user.profile.level > 1:
            print(f'User {user.profile.nickname} login.')
            auth.login(request, user)
            return HttpResponseRedirect('/gma/')
        else:
            print(f'User {user.profile.nickname} logout.')
            auth.logout(request)
            return HttpResponseRedirect('/mp/about/')

    def post(self, request):
        # Log.debug(f'WX POST: {request.body}')
        response = wx_impl.handle_wx_post(request)
        return response


if __name__ == '__main__':
    print('ok')
